package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/manifoldco/promptui"
	"github.com/spf13/viper"
)

// ANSI Color code
const (
	ColorReset = "\033[0m"
	ColorGreen = "\033[32m"
	ColorRed   = "\033[31m"
)

// Config configuration file structure
type Config struct {
	APIKey      string `mapstructure:"api_key"`
	APIEndpoint string `mapstructure:"api_endpoint"`
	Model       string `mapstructure:"model"`
	MaxDiffSize int    `mapstructure:"max_diff_size"`
	Style       string `mapstructure:"style"`
}

// CommitRequest doubao API Request Structure
type CommitRequest struct {
	Model       string    `json:"model"`
	Messages    []Message `json:"messages"`
	Temperature float64   `json:"temperature"`
	MaxTokens   int       `json:"max_tokens"`
}

// Message message structure
type Message struct {
	Role    string `json:"role"`
	Content string `json:"content"`
}

// CommitResponse doubao API Response Structure
type CommitResponse struct {
	Choices []struct {
		Message struct {
			Content string `json:"content"`
		} `json:"message"`
	} `json:"choices"`
}

// customReader 自定义输入读取器，将 Ctrl+J 和 Ctrl+K 转换为上下键
type customReader struct {
    reader io.Reader
}

func (cr *customReader) Read(p []byte) (n int, err error) {
    buf := make([]byte, 1)
    n, err = cr.reader.Read(buf)
    if err != nil {
        return n, err
    }

    // 检测 Ctrl+J (ASCII 10) 和 Ctrl+K (ASCII 11)
    switch buf[0] {
    case 10: // Ctrl+J -> 模拟向下键 (ESC [ B)
        copy(p, []byte{27, 91, 66}) // ANSI 序列: \x1b[B
        return 3, nil
    case 11: // Ctrl+K -> 模拟向上键 (ESC [ A)
        copy(p, []byte{27, 91, 65}) // ANSI 序列: \x1b[A
        return 3, nil
    default:
        copy(p, buf)
        return n, err
    }
}

// Close 实现 io.ReadCloser 的 Close 方法
func (cr *customReader) Close() error {
    // os.Stdin 不需要关闭，返回 nil
    return nil
}

func main() {
	isRepo, err := IsGitRepo()
	if err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	if !isRepo {
		fmt.Println("The current directory is not in the Git project")
		os.Exit(1)
	}

	// initialization configuration
	config, err := loadConfig()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to load config: %v\n", err)
		os.Exit(1)
	}

	// obtain Git diff
	diff, err := getGitDiff()
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to get git diff: %v\n", err)
		os.Exit(1)
	}

	if len(diff) == 0 {
		fmt.Println("No changes to commit.")
		os.Exit(0)
	}

	// Check diff size
	// if len(diff) > config.MaxDiffSize {
	//     fmt.Fprintf(os.Stderr, "Diff size exceeds max limit (%d bytes)\n", config.MaxDiffSize)
	//     os.Exit(1)
	// }

	// Generate commit message
	commitMsg, err := generateCommitMessage(config, diff)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to generate commit message: %v\n", err)
		os.Exit(1)
	}

	// interaction confirmation
	finalMsg, err := confirmCommitMessage(commitMsg, config)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to confirm commit message: %v\n", err)
		os.Exit(1)
	}

	// execute git commit
	err = executeGitCommit(finalMsg)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to commit: %v\n", err)
		os.Exit(1)
	}

	fmt.Println("Commit successful!")
}

// loadConfig Load configuration file
func loadConfig() (Config, error) {

	homeDir, err := os.UserHomeDir()
	if err != nil {
		panic(fmt.Errorf("failed to get home dir: %v", err))
	}

	configPath := filepath.Join(homeDir, ".config", "gitmessage")
	viper.AddConfigPath(configPath)
	viper.SetConfigName("config")
	viper.SetConfigType("yaml")

	// default configuration
	viper.SetDefault("api_endpoint", "https://gateway.theturbo.ai/v1/chat/completions")
	viper.SetDefault("model", "Doubao-1.5-pro-32k")
	viper.SetDefault("max_diff_size", 10000)
	viper.SetDefault("style", "angular")

	var config Config
	if err := viper.ReadInConfig(); err != nil {
		if _, ok := err.(viper.ConfigFileNotFoundError); ok {
			fmt.Println("Config file not found")
			return config, err
		}
		return config, err
	}

	if err := viper.Unmarshal(&config); err != nil {
		return config, err
	}

	// Make sure the API key exists
	if config.APIKey == "" {
		return config, fmt.Errorf("api_key is required in config")
	}

	return config, nil
}

// getGitDiff Get git diff content
func getGitDiff() (string, error) {
	cmd := exec.Command("git", "diff", "--cached")
	var out bytes.Buffer
	cmd.Stdout = &out
	cmd.Stderr = os.Stderr
	err := cmd.Run()
	if err != nil {
		return "", err
	}
	return out.String(), nil
}

// generateCommitMessage Call doubao API to generate commit message
func generateCommitMessage(config Config, diff string) (string, error) {
	// Construction prompts, support Chinese and English
	prompt := fmt.Sprintf(
		`根据以下Git diff，生成一个简洁的Git提交消息，使用英语. 注意输出的信息只有git提交信息，不要有其他冗余信息。消息需清晰、准确，每行长度控制在 72 个字符以内:

%s`, diff)

	reqBody := CommitRequest{
		Model: config.Model,
		Messages: []Message{
			{
				Role:    "user",
				Content: prompt,
			},
		},
		Temperature: 0.7,
		MaxTokens:   100,
	}

	// retry mechanism
	const maxRetries = 3
	var lastErr error

	for attempt := 1; attempt <= maxRetries; attempt++ {
		jsonData, err := json.Marshal(reqBody)
		if err != nil {
			return "", err
		}

		req, err := http.NewRequest("POST", config.APIEndpoint, bytes.NewBuffer(jsonData))
		if err != nil {
			return "", err
		}

		req.Header.Set("Content-Type", "application/json")
		req.Header.Set("Accept", "application/json")
		req.Header.Set("Authorization", "Bearer "+config.APIKey)

		client := &http.Client{Timeout: 30 * time.Second}
		resp, err := client.Do(req)
		if err != nil {
			lastErr = err
			fmt.Fprintf(os.Stderr, "Attempt %d failed: %v\n", attempt, err)
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt) * time.Second)
				continue
			}
			return "", lastErr
		}
		defer resp.Body.Close()

		if resp.StatusCode != http.StatusOK {
			lastErr = fmt.Errorf("API request failed with status: %s", resp.Status)
			fmt.Fprintf(os.Stderr, "Attempt %d failed: %v\n", attempt, lastErr)
			if attempt < maxRetries {
				time.Sleep(time.Duration(attempt) * time.Second)
				continue
			}
			return "", lastErr
		}

		body, err := io.ReadAll(resp.Body)
		if err != nil {
			return "", err
		}

		var commitResp CommitResponse
		if err := json.Unmarshal(body, &commitResp); err != nil {
			return "", err
		}

		if len(commitResp.Choices) == 0 {
			return "", fmt.Errorf("no commit message generated")
		}

		return strings.TrimSpace(commitResp.Choices[0].Message.Content), nil
	}

	return "", lastErr
}

// confirmCommitMessage Interactive confirm commit message
func confirmCommitMessage(msg string, config Config) (string, error) {
	fmt.Println("Generated commit message:")
	fmt.Printf("%s%s%s\n", ColorGreen, msg, ColorReset)

	prompt := promptui.Select{
		Label:    "Choose an action",
		HideHelp: true,
		Items:    []string{"Accept", "Edit", "Regenerate", "Cancel"},
	}

	customStdin := &customReader{reader: os.Stdin}
	prompt.Stdin = customStdin

	_, result, err := prompt.Run()
	if err != nil {
		return "", err
	}

	switch result {
	case "Accept":
		return msg, nil
	case "Edit":
		edited, err := EditStringWithVim(msg)
		if err != nil {
			return "", err
		}
		return edited, nil
	case "Regenerate":
		// Retrieve the diff and generate the message
		diff, err := getGitDiff()
		if err != nil {
			return "", err
		}
		newMsg, err := generateCommitMessage(config, diff)
		if err != nil {
			return "", err
		}
		return confirmCommitMessage(newMsg, config)
	case "Cancel":
		return "", fmt.Errorf("commit cancelled by user")
	}

	return "", fmt.Errorf("unexpected action")
}

// executeGitCommit execute git commit
func executeGitCommit(msg string) error {
	cmd := exec.Command("git", "commit", "-m", msg)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

// IsGitRepo Determine whether the current directory is a Git project
func IsGitRepo() (bool, error) {
	// Get the current working directory
	dir, err := os.Getwd()
	if err != nil {
		return false, err
	}

	// Look up the .git directory
	for {
		// Check if the .git directory exists
		if _, err := os.Stat(filepath.Join(dir, ".git")); err == nil {
			return true, nil
		}

		// Get parent directory
		parent := filepath.Dir(dir)
		// The directory no longer changes, reaching the root directory
		if parent == dir {
			break
		}

		dir = parent
	}

	return false, nil
}

// EditStringWithVim opens vim to edit the input string and returns the edited string
func EditStringWithVim(input string) (string, error) {
	// Create a temporary file
	tmpFile, err := os.CreateTemp("", "edit-*.txt")
	if err != nil {
		return "", fmt.Errorf("failed to create temp file: %v", err)
	}

	// Ensure temp file is removed even on error
	fileName := tmpFile.Name()
	defer func() {
		if err := os.Remove(fileName); err != nil && !os.IsNotExist(err) {
			fmt.Fprintf(os.Stderr, "Warning: failed to remove temp file %s: %v\n", fileName, err)
		}
	}()

	// Write the input string to the temp file
	if err := os.WriteFile(fileName, []byte(input), 0644); err != nil {
		return "", fmt.Errorf("failed to write to temp file: %v", err)
	}

	// Launch vim to edit the temp file
	cmd := exec.Command("vim", fileName)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return "", fmt.Errorf("failed to run vim: %v", err)
	}

	// Read the edited content from the temp file
	editedContent, err := os.ReadFile(fileName)
	if err != nil {
		return "", fmt.Errorf("failed to read temp file: %v", err)
	}

	return string(editedContent), nil
}
